/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2018-2022.
 * Description: support oom_extend feature
 * Author: fanglinxu <fanglinxu@huawei.com>
 * Create: 2018-08-25
 */

#include <../fs/mount.h>
#include <linux/kernel.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/writeback.h>
#include <linux/sysctl.h>
#include <linux/gfp.h>
#include <linux/seq_file.h>
#include <linux/module.h>
#include <linux/pagevec.h>
#include <linux/sort.h>
#include <linux/security.h>
#include <linux/rtos_oom_extend.h>
#include <linux/rtos_super.h>
#include <linux/mount.h>
#ifdef CONFIG_RTOS_PAGECACHE_INFO
#include <linux/proc_fs.h>
#endif

#define MAX_PAGECACHE_COUNT 50
#define SHOW_ALL_PAGECACHE (-1)

struct pagecache_nrpages {
	struct inode *inode;
	unsigned long nrpages;
};

static char pc_path_buff[PATH_MAX];
static struct pagecache_nrpages pagecache_nrpages_info[MAX_PAGECACHE_COUNT];

struct mutex pagecache_lock;

static int pagecache_compare(const void *a, const void *b)
{
	if (((struct pagecache_nrpages *)a)->nrpages > ((struct pagecache_nrpages *)b)->nrpages)
		return -1;
	if (((struct pagecache_nrpages *)a)->nrpages < ((struct pagecache_nrpages *)b)->nrpages)
		return 1;
	return 0;
}

static void pagecache_swap(void *a, void *b, int size)
{
	struct pagecache_nrpages *entry1 = a;
	struct pagecache_nrpages *entry2 = b;
	struct pagecache_nrpages tmp;

	BUG_ON(size != sizeof(*entry1));

	tmp.inode = entry1->inode;
	tmp.nrpages = entry1->nrpages;
	entry1->inode = entry2->inode;
	entry1->nrpages = entry2->nrpages;
	entry2->inode = tmp.inode;
	entry2->nrpages = tmp.nrpages;
}

static inline void print_pagecache(char *path, unsigned long nrpages,
		struct seq_file *s)
{
	if (s)
		seq_printf(s, "  %s : nrpages = %lu.\n", path, nrpages);
	else
		pr_info("  %s : nrpages = %lu.\n", path, nrpages);
}

static char *dentry_to_path(struct dentry *dentry, struct inode *inode)
{
	struct super_block *sb = inode->i_sb;
	struct mount *mnt;
	struct path tmp_path;
	struct dentry *root_dentry = dentry;
	struct vfsmount *vfsmnt;
	char *path = NULL;
	bool found = false;

	while (!IS_ROOT(root_dentry))
		root_dentry = root_dentry->d_parent;

	if (!spin_trylock(&mount_lock.lock))
		return NULL;

	list_for_each_entry(mnt, &sb->s_mounts, mnt_instance) {
		vfsmnt = &mnt->mnt;
		tmp_path.mnt = vfsmnt;
		tmp_path.dentry = dentry;
		if (root_dentry == vfsmnt->mnt_root) {
			found = true;
			break;
		}
	}
	spin_unlock(&mount_lock.lock);

	if (found) {
		path = d_path(&tmp_path, pc_path_buff, PATH_MAX);
		if (!IS_ERR_OR_NULL(path))
			return path;
	}

	return NULL;
}

static char *inode_to_path(struct inode *inode)
{
	struct dentry *p;
	char *path = NULL;
	struct vm_area_struct *vma;
	struct address_space *mapping;
	struct mm_struct *mm;
	struct file *vm_file;

	p = d_find_alias(inode);
	if (p) {
		path = dentry_to_path(p, inode);
		dput(p);
		if (path)
			return path;
	}

	/* If we can't find the dentry, try to use rmap to find vm_file. */
	mapping = inode->i_mapping;
	if (!mapping || !down_read_trylock(&mapping->i_mmap_rwsem))
		return NULL;

	vma = vma_interval_tree_iter_first(&mapping->i_mmap, 0, ULONG_MAX);
	up_read(&mapping->i_mmap_rwsem);
	if (!vma)
		return NULL;

	mm = vma->vm_mm;
	if (mmap_read_trylock(mm)) {
		vm_file = vma->vm_file;
		if (!vm_file) {
			mmap_read_unlock(mm);
			return NULL;
		}
		path = d_path(&vm_file->f_path, pc_path_buff, PATH_MAX);
		if (IS_ERR(path)) {
			mmap_read_unlock(mm);
			return NULL;
		}
		mmap_read_unlock(mm);
	}

	return path;
}
static int sort_pagecache_nrpages_info(struct super_block *sb, struct inode *inode,
	    long *sort_pos, long print_count)
{
	if (*sort_pos < print_count) {
		pagecache_nrpages_info[*sort_pos].inode = inode;
		pagecache_nrpages_info[(*sort_pos)++].nrpages = inode->i_mapping->nrpages;
	} else {
		/* sort and insert tail. */
		if (pagecache_nrpages_info[*sort_pos - 1].nrpages < inode->i_mapping->nrpages) {
			spin_unlock(&sb->s_inode_list_lock);
			iput(pagecache_nrpages_info[*sort_pos - 1].inode);
			spin_lock(&sb->s_inode_list_lock);

			pagecache_nrpages_info[*sort_pos - 1].inode = inode;
			pagecache_nrpages_info[*sort_pos - 1].nrpages = inode->i_mapping->nrpages;
		} else {
			iput(inode);
			return 1;	/* ignore sort() */
		}
	}
	sort(pagecache_nrpages_info, *sort_pos, sizeof(struct pagecache_nrpages),
		pagecache_compare, pagecache_swap);
	return 0;
}

static void inode_list_pagecache_print(struct super_block *sb,
	struct inode *inode, struct inode **toput_inode_p,
	long *sort_pos, long print_count, struct seq_file *s)
{
	char *path = NULL;

	spin_lock(&inode->i_lock);
	if ((inode->i_state & (I_FREEING|I_CLEAR|I_WILL_FREE|I_NEW)) ||
	   (inode->i_mapping->nrpages == 0)) {
		spin_unlock(&inode->i_lock);
		return;
	}

	__iget(inode);
	spin_unlock(&inode->i_lock);
	spin_unlock(&sb->s_inode_list_lock);
	path = inode_to_path(inode);
	spin_lock(&sb->s_inode_list_lock);
	if (!path) {
		iput(inode);
		return;
	}
	if (print_count > 0 && print_count <= MAX_PAGECACHE_COUNT) {
		sort_pagecache_nrpages_info(sb, inode, sort_pos, print_count);
	} else if (print_count == SHOW_ALL_PAGECACHE) {
		iput(*toput_inode_p);
		*toput_inode_p = inode;
		print_pagecache(path, inode->i_mapping->nrpages, s);
	} else {
		BUG_ON(1);
	}
}

static void show_superblock_info(struct super_block *sb,
	long *sort_pos, long print_count, struct seq_file *s)
{
	struct inode *inode = NULL;
	struct inode *toput_inode = NULL;

	if (print_count == 0)
		return;

	spin_lock(&sb->s_inode_list_lock);
	list_for_each_entry(inode, &sb->s_inodes, i_sb_list)
		inode_list_pagecache_print(sb, inode, &toput_inode, sort_pos, print_count, s);

	spin_unlock(&sb->s_inode_list_lock);
	iput(toput_inode);
}

static void put_inodes(long sort_pos)
{
	long i;

	for (i = 0; i < sort_pos; i++)
		iput(pagecache_nrpages_info[i].inode);
}

static void destroy_sb_writers(struct super_block *s)
{
	int i;

	for (i = 0; i < SB_FREEZE_LEVELS; i++)
		percpu_free_rwsem(&s->s_writers.rw_sem[i]);
}

static inline void destroy_super(struct super_block *s)
{
	destroy_sb_writers(s);
	security_sb_free(s);
	WARN_ON(!list_empty(&s->s_mounts));
	kfree(s->s_subtype);
	kfree(s);
}

static void _put_super(struct super_block *sb)
{
	if (!--sb->s_count) {
		list_del_init(&sb->s_list);
		destroy_super(sb);
	}
}

static void path_nrpages_print(long sort_pos, struct seq_file *s)
{
	long i;

	for (i = 0; i < sort_pos; i++) {
		struct inode *inode = pagecache_nrpages_info[i].inode;
		char *path = inode_to_path(inode);
		if (!path)
			return;
		print_pagecache(path, inode->i_mapping->nrpages, s);
	}
}

static void deal_sb_list(long print_count, long *sort_pos_p, struct seq_file *s)
{
	struct super_block *sb = NULL;
	struct super_block *p = NULL;
	int trylock_success;

	spin_lock(&sb_lock);

	list_for_each_entry(sb, &super_blocks, s_list) {
		if (hlist_unhashed(&sb->s_instances))
			continue;
		trylock_success = 0;
		sb->s_count++;
		spin_unlock(&sb_lock);
		if (down_read_trylock(&sb->s_umount)) {
			trylock_success = 1;
			if (sb->s_root)
				show_superblock_info(sb, sort_pos_p, print_count, s);

			up_read(&sb->s_umount);
		} else {
			pr_info("down_read_trylock(&sb->s_umount) failed, ignore this super_blocks:%pK.\n",
				sb);
			sb->s_count--;
		}
		spin_lock(&sb_lock);
		if (p)
			_put_super(p);

		if (trylock_success)
			p = sb;
		else
			p = NULL;
	}

	if (p)
		_put_super(p);
	spin_unlock(&sb_lock);
}

int show_pagecache_info(long print_count)
{
	int err = 0;
	long sort_pos = 0;

	if (print_count == 0) {
		err = 0;
		goto out;
	}

	if (print_count > 0 && print_count <= MAX_PAGECACHE_COUNT) {
		memset(pagecache_nrpages_info, 0,
			sizeof(struct pagecache_nrpages) * MAX_PAGECACHE_COUNT);
	} else if (print_count != SHOW_ALL_PAGECACHE) {
		pr_err("Error, print_count should in [0, %d].\n", MAX_PAGECACHE_COUNT);
		err = -EINVAL;
		goto out;
	}
	deal_sb_list(print_count, &sort_pos, NULL);
	path_nrpages_print(sort_pos, NULL);
	put_inodes(sort_pos);
out:
	return err;
}
EXPORT_SYMBOL(show_pagecache_info);

#ifdef CONFIG_RTOS_PAGECACHE_INFO
static int proc_pagecache_show(struct seq_file *s, void *offset)
{
	long sort_pos = 0;

	mutex_lock(&pagecache_lock);
	deal_sb_list(oom_pagecache_count, &sort_pos, s);
	path_nrpages_print(sort_pos, s);
	put_inodes(sort_pos);
	mutex_unlock(&pagecache_lock);

	return 0;
}

static int pagecache_show_open(struct inode *inode, struct file *file)
{
	return single_open(file, proc_pagecache_show, NULL);
}

static const struct proc_ops pagecache_show_ops = {
	.proc_open         = pagecache_show_open,
	.proc_read         = seq_read,
	.proc_lseek        = seq_lseek,
	.proc_release      = single_release,
};

static int __init init_pagecache_info_procfs(void)
{
	struct proc_dir_entry *pe = NULL;

	pe = proc_create("pagecache", (mode_t)0400, NULL, &pagecache_show_ops);
	if (pe == NULL) {
		pr_err("proc_create pagecache failed.\n");
		return -ENOMEM;
	}
	mutex_init(&pagecache_lock);

	return 0;
}

__initcall(init_pagecache_info_procfs);
#endif
